<?php
class Exts_ExceptionFormatter {

        /**
         * Suppresses the standard viewRenderer and directly outputs the formatted
         * exception (HTML, CSS and JavaScript.
         *
         * @param ArrayObject $error The error handler array object for the currently controller. Generally accessed via $this->_getParam('error_handler') from within the controller.
         * @param Zend_Controller_Action_HelperBroker $viewHelper The main view helper for the current controller. Generally accessed via $this->_helper from within the controller.
         * @param boolean $extended A flag used to dertmine if the stack trace should be extended to contain interactive source code or not. Browser performance is degraded when this feature is enabled. Default is false.
         */

    static function display (ArrayObject $error, Zend_Controller_Action_HelperBroker $viewHelper, $extended = false)
    {
        $viewHelper->viewRenderer->setNoRender();

        $data = array();
        $data['errorMessage'] = $error['exception']->getMessage();
        $data['errorType'] = $error['type'];
        $data['errorCode'] = $error['exception']->getCode();
        $data['errorFilePath'] = $error['exception']->getFile();
        $data['errorLineNumber'] = $error['exception']->getLine();
        $data['errorLineNumberFormatted'] = self::addPadding( $error['exception']->getLine() );
        $data['errorCodeBlock'] = self::generateCodeBlock( $error['exception']->getLine(), $error['exception']->getFile() );

        $data['traceStack'] = array();
        foreach( $error['exception']->getTrace() as $trace) {
            if ($extended) {
                $trace['lineNumberFormatted'] = self::addPadding( isset($trace['line'])?$trace['line']:"line" );
                $trace['codeBlock'] = self::generateCodeBlock( isset($trace['line'])?$trace['line']:"line",
                    isset($trace['file'])?$trace['file']:"file" );
            }
            $data['traceStack'][] = $trace;
        }

        self::render($data, $extended);
    }

        /**
         * Handles the formatting of function arguments values for a given function
         * within a stack trace.
         * @access private
         * @param array $args An array of function arguments
         * @return string Formatted arguments ready for display in the output
         * template.
         */
    private static function formatArgValues ($args)
    {
        $values = array();
        foreach($args as $arg) {
            if (is_object($arg)) {
                $values[] = get_class($arg);
            } elseif (is_null($arg)) {
                $values[] = 'NULL';
            } elseif (is_array($arg)) {
                $values[] = 'Array('.count($arg).')';
            } elseif (is_string($arg)) {
                $values[] = "'$arg'";
            } else {
                $values[] = (string) $arg;
            }
        }
        return implode(', ', $values);
    }

        /**
         * Padds line numbers within the the source code using &amp;nbsp; to ensure
         * 5 positions.
         *
         * @access private
         * @todo This is a hacky way to go about this. Perhaps Ill revist it at
         * some point.
         * @param integer $number
         * @return string Number with appropriate padding applied
         */
    private static function addPadding ($number)
    {
        $formattedNumber = str_pad($number, 5, '_', STR_PAD_RIGHT);
        return str_replace('_', ' ', $formattedNumber );
    }

        /**
         * Loads the given file and highlights it via highlight_file method. It
         * then inserts formatted line numbers within the resulting and highlights
         * the error line.
         *
         * @access private
         * @param integer $errorLine The line on which the error occured
         * @param string $filePath The full file path to the source code in which
         * the error occured
         * @return string The result of of the highlight_file function (source code
         * formatted with HTML) with line numbers spliced in and wrapped in a
         * containing div for formatting and to provide a javascript DOM hook.
         */
    private static function generateCodeBlock ($errorLine, $filePath)
    {
        if ( is_file( $filePath ) ) {
            $lines = explode( '<br />', highlight_file($filePath, true) );
        } else {
            $lines = array();
        }
        $errorID = '';
        for($n = 0; $n < count($lines); $n++) {
            $lineNumber = $n+1;
            $paddedNumber =  self::addPadding( $lineNumber );
            $errorClass = '';
            list($errorClass, $errorID) = ($lineNumber == $errorLine) ? array('errorLine', md5( $errorLine.$filePath ) ) : array('',$errorID);
            $lines[ $n ] = "<span class=\"lineNumbers $errorClass\" id=\"$errorID\">$paddedNumber</span>".$lines[ $n ];
        }
        return "<div class=\"codeFile\" errorid=\"$errorID\">".implode("<br />\n", $lines).'</div>';
    }

        /**
         * Outputs formatted HTML directly to the browser. This method defines the
         * HTML (CSS & JavaScript) template used for output using heredoc.
         *
         * @access private
         * @param array $data A hash of formatted strings ready for display
         * @param boolean $extended A flag used to dertmine if the stack trace should be extended to contain interactive source code or not.
         */
    private static function render($data, $extended)
    {
        $compiledTrace = '';
        foreach($data['traceStack'] as $trace) {
            $compiledTrace .= '<li class="codeBlock">'."\n";
            if ($extended) {
                $compiledTrace .= '<div class="filePath"><a class="openLink" href="javascript://">open</a>'
                .(isset( $trace['file'] )?$trace['file']:"file").'</div>'."\n";
            } else {
                $compiledTrace .= '<div class="filePath"><span class="openLink">'
                . (isset($trace['line'])?$trace['line']:"line")
                .'</span>'. (isset($trace['file'])?$trace['file']:"file")
                .'</div>'."\n";
            }
            $compiledTrace .= '<div class="functionCall">'
            . (isset($trace['class'])?$trace['class']:"class") . '->'
            . (isset($trace['function'])?$trace['function']:"function")
            . '(' . self::formatArgValues(isset($trace['args'])?$trace['args']:"args") . ')</div>'."\n";
            if ($extended) {
                $compiledTrace .= (isset($trace['codeBlock'])?$trace['codeBlock']:"codeBlock") ."\n";
            }
            $compiledTrace .= '</li>'."\n";
        }

        echo <<<EOT
  <script>
        function updateCodeBlocks() {
                $$('#error .codeBlock A.openLink').each(function(link){
                        link.observe('click',toggleCodeFile);
                });

                $$('#error .codeBlock').each(function(element){
                        element.select('DIV.codeFile').each(function(codeFile){
                                $( codeFile.readAttribute('errorid') ).scrollIntoView();
                        });
                        $('error').scrollIntoView(true);
                });
        }

        function toggleCodeFile(event) {
                event.target.blur();
                $(event.target.parentNode).adjacent('DIV.codeFile')[0].toggleClassName('open');
                $( $(event.target.parentNode).adjacent('DIV.codeFile')[0].readAttribute('errorid') ).scrollIntoView();
                event.target.innerHTML = (event.target.innerHTML == 'open') ? 'close' : 'open';
        }


        document.observe("dom:loaded", updateCodeBlocks);
  </script>
  <style>
        #error
                {font-family:Verdana,Arial,Helvetica,sans-serif;font-size:10pt; color:#333;margin:0;padding:0;}
        #error .title
                {border-bottom:1px solid #0e698b;overflow:hidden;height: 168px; width: 820px;;text-align:right;}
        #error .title .prefix
                {display:block;font-size:20pt;font-weight:bold;margin:20pt 468pt 0 0;padding:0;color:#0e698b;}
        #error .title .message
                {clear:both;width:800px;font-size:12pt;color:#000;position:absolute;top: 218px; left: 226px;}
        #error .type
                {color:#C8EFFA; font-size:9px; background-color:#334c60; padding: 1px 5px 3px 10px; border-top:2px solid #2e4456; border-bottom:1px solid #617789; margin-bottom:10px;}
        #error .head
                {border-bottom:1px dotted #ADBAC6;color:#0088B5;font-size:16pt;margin:15px 15px 15px 10px;}

        #error .codeBlock
                {font-family:"Courier New",Courier,monospace;font-size:8pt;font-weight:bold;color:#7B8D9A;margin:15px 15px 15px 10px;}
        #error .filePath
                {color:#334c60;background-color:#D3E0EB;padding:2pt 0 2pt 0;}
        #error .functionCall
                {color:#FFF;background-color:#10759C;padding:2pt 0 2pt 35pt;font-size:8pt;font-weight:normal;border:1px solid #D3E0EB;border-top:none;}
        #error .codeFile
                {height:15pt;overflow:hidden;background-color:#EDF7FF;border:1px solid #D3E0EB;border-top:none;}
        #error .codeBlock .open
                {height:125pt;overflow:auto;}
        #error .codeFile .lineNumbers
                {color:#7B8D9A;background-color:#D3E0EB;border:1px solid #EDF7FF;padding-left:3pt;margin-right:3pt;}
        #error .codeFile .errorLine
                {color:#FFF;background-color:#A00;}
        #error .codeBlock .openLink
                {float:left;padding:1pt 0 2pt 2pt;margin:-2pt 5pt 0 0;width:28pt;background-color:#10759C;color:#FFF;font-size:8pt;
                        library/Exts/ExceptionFormatter.phpborder-left:1px solid #D3E0EB;border-top:1px solid #D3E0EB;}
        #error .codeBlock A.openLink:hover
                {color:#10759C;background-color:#EDF7FF;}

        #error .trace
                {margin:15px 15px 15px 10px;}
        #error .trace LI
                {margin-right:0;}
  </style>

<div id="error">

<div class="title"><span class="prefix">Fatal Error</span> <div class="message">{$data['errorMessage']}</div></div>
<div class="type">{$data['errorType']} : <span class="code">{$data['errorCode']}</span></div>

<div class="head">{main}</div>

<div class="codeBlock">
        <div class="filePath"><a class="openLink" href="javascript://">open</a>{$data['errorFilePath']}</div>
        {$data['errorCodeBlock']}
</div>

<div class="head">Stack trace</div>
<div class="trace">
        <ol>
        $compiledTrace
        </ol>
</div>

</div>
EOT;
    }
}
